import asyncio
import logging
import json
import os
import time
from pathlib import Path
from datetime import datetime
from threading import Lock
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError

# 配置日志（仅输出到文件）
def setup_logger():
    today = datetime.now().strftime("%Y%m%d")
    log_filename = f"monitor_{today}.log"
    
    log_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    
    file_handler = logging.FileHandler(log_filename, encoding='utf-8')
    file_handler.setFormatter(log_format)
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    if logger.hasHandlers():
        logger.handlers.clear()
    logger.addHandler(file_handler)
    
    return logger

# 记录配置文件最后修改时间和线程锁
config_last_modified = 0
config_lock = Lock()  # 确保配置更新时的线程安全
account_sessions = {}  # {account_name: {"session": (page, context), "task": task, "config": account_config}}
stop_event = None  # 全局停止事件

logger = setup_logger()

# 读取配置文件
def load_config(config_path="config.json"):
    try:
        if not Path(config_path).exists():
            raise FileNotFoundError(f"配置文件 {config_path} 不存在")
        
        with open(config_path, "r", encoding="utf-8") as f:
            config = json.load(f)
        
        required_keys = ["global", "accounts"]
        for key in required_keys:
            if key not in config:
                raise ValueError(f"配置文件缺少必要字段: {key}")
        
        logger.info("配置文件加载成功")
        return config
    except Exception as e:
        logger.error(f"加载配置文件失败: {str(e)}")
        raise

config = load_config()
config_last_modified = os.path.getmtime("config.json")  # 记录初始修改时间
GLOBAL_CONFIG = config["global"]
ACCOUNTS = config["accounts"]

async def handle_unread_messages(page, account_name, replied_users):
    """处理未读消息的具体逻辑（优化发送效率）"""
    try:
        # 1. 优化定位器性能：通过im-container上层容器定位消息列表
        im_container = page.locator(".im-container")
        message_list = im_container.locator(".List")
        unread_locator = message_list.locator(".ant-scroll-number-only-unit.current")
        current_count = await unread_locator.count()
        
        if current_count > 0:
            logger.info(f"账号 {account_name} 发现列表 {current_count} 条未读消息，开始处理...")
            
            # 预获取输入框定位器（避免重复创建）
            chat_input = page.locator('#chartbox-editor')
            
            for i in range(current_count):
                try:
                    element = unread_locator.nth(i)
                    
                    # 滚动到元素（缩短超时）
                    try:
                        await element.scroll_into_view_if_needed(timeout=800)
                    except PlaywrightTimeoutError:
                        # 滚动失败时添加快照
                        snapshot_path = f"snapshots/{account_name}_scroll_failed_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
                        os.makedirs(os.path.dirname(snapshot_path), exist_ok=True)
                        await page.screenshot(path=snapshot_path)
                        logger.warning(f"账号 {account_name} 元素滚动失败，已保存快照至 {snapshot_path}，跳过")
                        await handle_possible_popup(page, account_name)
                        continue
                    
                    # 点击未读消息（缩短超时）
                    try:
                        await element.click(timeout=800)
                    except PlaywrightTimeoutError:
                        # 点击失败时添加快照
                        snapshot_path = f"snapshots/{account_name}_click_failed_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
                        os.makedirs(os.path.dirname(snapshot_path), exist_ok=True)
                        await page.screenshot(path=snapshot_path)
                        logger.warning(f"账号 {account_name} 元素点击失败，已保存快照至 {snapshot_path}，跳过")
                        await handle_possible_popup(page, account_name)
                        continue
                        
                    logger.info(f"账号 {account_name} 已点击第 {i+1} 条未读消息")
                    
                    # 获取用户昵称（修改：从消息元素中提取）
                    # 定位消息元素（sc-fzqNqU hVQTgG），取第一个即可
                    message_element = page.locator(".sc-fzqNqU.hVQTgG").nth(0)
                    if await message_element.count() == 0:
                        logger.warning(f"账号 {account_name} 未找到消息元素，跳过")
                        continue
                    
                    # 从消息元素中提取昵称
                    nickname_element = message_element.locator(".text-nickname")
                    if await nickname_element.count() == 0:
                        logger.warning(f"账号 {account_name} 未找到用户昵称元素，跳过")
                        continue
                    
                    user_nickname = await nickname_element.inner_text(timeout=800)
                    logger.info(f"当前聊天用户: {user_nickname}")
                    
                    today = datetime.now().strftime("%Y-%m-%d")
                    if user_nickname not in replied_users or replied_users[user_nickname]["date"] != today:
                        replied_users[user_nickname] = {"date": today, "count": 0}
                    
                    current_count = replied_users[user_nickname]["count"]
                    max_replies = GLOBAL_CONFIG["max_daily_replies"]
                    if current_count >= max_replies:
                        logger.info(f"账号 {account_name} 对 {user_nickname} 回复达上限({max_replies})")
                        continue
                    
                    # 发送回复
                    next_count = current_count + 1
                    welcome_msg = GLOBAL_CONFIG["welcome_messages"].get(
                        str(next_count),
                        GLOBAL_CONFIG["welcome_messages"][str(max_replies)]
                    )
                    
                    try:
                        # 合并操作步骤，减少等待时间
                        await asyncio.gather(
                            chat_input.wait_for(state='visible', timeout=800),
                            chat_input.click(timeout=800)
                        )
                        
                        # 直接填充并发送
                        await chat_input.type(welcome_msg, delay=0)  # 极低延迟输入
                        await chat_input.press('Enter', timeout=800)
                        logger.info(f"账号 {account_name} 已发送消息: {welcome_msg}")
                        replied_users[user_nickname]["count"] = next_count
                        logger.info(f"账号 {account_name} 对 {user_nickname} 今日回复 {next_count}/{max_replies} 次")

                    except PlaywrightTimeoutError:
                        logger.error(f"账号 {account_name} 输入框操作超时")
                    except Exception as e:
                        logger.error(f"账号 {account_name} 发送消息失败: {str(e)}")
                        
                except Exception as e:
                    logger.error(f"账号 {account_name} 处理第 {i+1} 条消息出错：{str(e)}")
                    continue
    except Exception as e:
        logger.error(f"账号 {account_name} 处理未读消息时发生错误：{str(e)}")
        
async def monitor_page_changes(page, account_name, stop_event):
    """监控页面变化（使用id="login-component"简化登录状态检测）"""
    logger.info(f"开始监控账号 {account_name} 的页面变化...")
    
    replied_users = {}  # {用户昵称: {"date": "YYYY-MM-DD", "count": 0}}
    monitoring = True
    
    try:
        # 登录状态验证：仅通过id="login-component"判断（简洁稳定）
        max_attempts = 3
        is_logged_in = False
        
        for attempt in range(max_attempts):
            try:
                # 检测登录组件（id唯一且固定，优先级最高）
                login_component = page.locator("#login-component")
                # 等待登录组件出现（最多5秒）
                await login_component.wait_for(state="visible", timeout=5000)
                
                # 若登录组件可见 → 未登录
                logger.warning(f"账号 {account_name} 检测到登录组件，未登录（尝试 {attempt+1}/{max_attempts}）")
                await page.reload(timeout=2000)  # 重试刷新
                
            except PlaywrightTimeoutError:
                # 超时未找到登录组件 → 已登录
                is_logged_in = True
                logger.info(f"账号 {account_name} 未检测到登录组件，已登录（尝试 {attempt+1}/{max_attempts}）")
                break
            except Exception as e:
                if attempt < max_attempts - 1:
                    logger.warning(f"登录检测出错，重试中（尝试 {attempt+1}/{max_attempts}）：{str(e)}")
                    await page.reload(timeout=2000)
                else:
                    raise Exception(f"登录检测失败，已尝试 {max_attempts} 次：{str(e)}")
        
        # 若最终未登录，直接退出并通知
        if not is_logged_in:
            send_ios_push(
                key="SjrCnTXx3tLNM7sNoVLXvL",
                title=f"快手账号监控失败通知",
                message=f"账号 {account_name} 未登录，无法继续监控"
            )
            logger.info(f"账号 {account_name} 因未登录，退出监控任务")
            return
            
        # 弹窗处理
        await asyncio.sleep(3)  
        await page.mouse.click(x=950, y=550)  # 点击右下角附近
        await asyncio.sleep(1)  
        await page.mouse.click(x=950, y=550)  # 点击右下角附近
        
        # 1. 未读消息列表定位器（用于列表页检测）
        message_list = page.locator(".im-container .List")
        unread_locator = message_list.locator(".ant-scroll-number-only-unit.current")
        
        # 2. 当前会话相关定位器（用于聊天窗口检测）
        base_message_element = page.locator(".sc-fzqNqU.hVQTgG").nth(0)
        nickname_element = base_message_element.locator(".text-nickname")
        message_container = page.locator(".sc-fzqARJ.eLpUJW")
        message_elements = message_container.locator(".sc-fzqNqU:not(.sc-fznMAR)")  
        chat_input = page.locator('#chartbox-editor')
        
        # 记录初始未读消息数量
        last_unread_count = await unread_locator.count()
        if last_unread_count > 0:
            await handle_unread_messages(page, account_name, replied_users)
        
        # --------------------------
        # 事件驱动循环（替代轮询）
        # --------------------------
        logger.info(f"账号 {account_name} 启动事件驱动监控模式")
        POPUP_CHECK_INTERVAL = 30 #  定时弹窗检测间隔（秒）
        last_popup_check = time.time()

        while monitoring and not stop_event.is_set():
            if account_name not in account_sessions:
                logger.info(f"账号 {account_name} 会话已关闭，停止监控")
                break
            # 定期检查弹窗
            current_time = time.time()
            if current_time - last_popup_check > POPUP_CHECK_INTERVAL:
                await handle_possible_popup(page, account_name)
                last_popup_check = current_time

            # 同时监听两种事件：未读消息数量变化 OR 当前会话新消息
            try:
                # 用 asyncio.wait_for 控制单次监听超时（避免永久阻塞）
                done, pending = await asyncio.wait(
                    [
                        # 事件1：监听未读消息数量变化（列表页）
                        asyncio.create_task(wait_for_unread_change(unread_locator, last_unread_count)),
                        # 事件2：监听当前会话新消息（聊天窗口）
                        asyncio.create_task(wait_for_new_message(message_elements))
                    ],
                    timeout=10,  # 10秒无事件则刷新监听
                    return_when=asyncio.FIRST_COMPLETED  # 任一事件触发则继续
                )
                
                # 处理已完成的事件
                for future in done:
                    event_result = future.result()
                    if event_result["type"] == "unread_change":
                        # 未读消息数量变化：更新计数并处理
                        last_unread_count = event_result["new_count"]
                        await handle_unread_messages(page, account_name, replied_users)
                    elif event_result["type"] == "new_message":
                        # 当前会话新消息：处理新消息
                        await process_new_chat_message(
                            page, account_name, replied_users,
                            base_message_element, nickname_element,
                            message_elements, chat_input
                        )
                
                # 取消未完成的任务（避免资源泄漏）
                for future in pending:
                    future.cancel()
            
            except asyncio.TimeoutError:
                # 超时无事件：仅日志提示，不做处理
                logger.debug(f"账号 {account_name} 10秒内无事件，继续监听")
            except Exception as e:
                logger.error(f"账号 {account_name} 事件监听出错: {str(e)}")
        
    except Exception as e:
        logger.error(f"账号 {account_name} 监控出错：{str(e)}")
    finally:
        monitoring = False
        if account_name in account_sessions:
            del account_sessions[account_name]
        logger.info(f"账号 {account_name} 监控任务已停止")


# 事件监听辅助函数
async def wait_for_unread_change(unread_locator, last_count):
    """监听未读消息数量变化（列表页事件）"""
    while True:
        current_count = await unread_locator.count()
        if current_count != last_count:
            return {
                "type": "unread_change",
                "new_count": current_count
            }
        # 短暂等待后重试（降低CPU占用）
        await asyncio.sleep(0.1)

async def wait_for_new_message(message_elements):
    """监听当前会话新消息（聊天窗口事件）"""
    # 先获取当前消息总数
    initial_count = await message_elements.count()
    while True:
        current_count = await message_elements.count()
        if current_count > initial_count:
            # 新消息出现：返回最后一条消息的索引
            return {
                "type": "new_message",
                "new_count": current_count
            }
        # 短暂等待后重试（降低CPU占用）
        await asyncio.sleep(0.5)

async def process_new_chat_message(
    page, account_name, replied_users,
    base_message_element, nickname_element,
    message_elements, chat_input
):
    """处理当前会话新消息"""
    try:
        # 1. 检查是否在聊天窗口（避免无效操作）
        if await base_message_element.count() == 0:
            logger.debug(f"账号 {account_name} 未在聊天窗口，跳过新消息处理")
            return
        
        # 2. 获取当前聊天用户昵称
        if await nickname_element.count() == 0:
            logger.warning(f"账号 {account_name} 未找到用户昵称元素，跳过")
            return
        user_nickname = await nickname_element.inner_text(timeout=800)
        today = datetime.now().strftime("%Y-%m-%d")
        
        # 3. 初始化回复计数（每日重置）
        if user_nickname not in replied_users or replied_users[user_nickname]["date"] != today:
            replied_users[user_nickname] = {"date": today, "count": 0}
        
        # 4. 检查回复上限
        current_count = replied_users[user_nickname]["count"]
        max_replies = GLOBAL_CONFIG["max_daily_replies"]
        if current_count >= max_replies:
            logger.info(f"账号 {account_name} 对 {user_nickname} 已达回复上限({max_replies})")
            return
        
        # 5. 确认新消息是用户发送的（优化判断逻辑）
        last_message = message_elements.nth(-1)
        is_user_message = await is_user_sent_message(last_message)
        if not is_user_message:
            logger.debug(f"账号 {account_name} 最后一条消息是系统/机器人发送，跳过")
            return
        
        # 6. 发送回复
        next_count = current_count + 1
        welcome_msg = GLOBAL_CONFIG["welcome_messages"].get(
            str(next_count),
            GLOBAL_CONFIG["welcome_messages"][str(max_replies)]
        )
        
        await asyncio.gather(
            chat_input.wait_for(state='visible', timeout=800),
            chat_input.click(timeout=800)
        )
        await chat_input.type(welcome_msg, delay=0)
        await chat_input.press('Enter', timeout=800)
        
        replied_users[user_nickname]["count"] = next_count
        logger.info(f"账号 {account_name} 对 {user_nickname} 发送回复({next_count}/{max_replies}): {welcome_msg}")
        
    except Exception as e:
        logger.error(f"账号 {account_name} 处理新消息失败: {str(e)}")

async def is_user_sent_message(message_element):
    """优化：简化判断消息发送者的逻辑（替代原evaluate样式判断）"""
    try:
        # 方案1：通过classname判断（如果页面有区分用户/机器人的class）以后有再加
        # has_user_class = await message_element.locator(".user-message").count() > 0
        # if has_user_class:
        #     return True
        
        # 方案2：备用方案（如果无class，再用样式判断，减少evaluate调用）
        content_div = message_element.locator(".text-msg > div:nth-child(2)")
        justify_style = await content_div.evaluate("el => window.getComputedStyle(el).justifyContent")
        return justify_style.strip() == "flex-start"
    except Exception as e:
        logger.warning(f"判断消息发送者失败: {str(e)}")
        return False

async def handle_possible_popup(page, account_name):
    # 弹窗处理
    try:
        await asyncio.sleep(1)  
        await page.mouse.click(x=950, y=550)  # 点击右下角附近
        
    except Exception as e:
        logger.warning(f"处理弹窗出错（忽略）：{str(e)}")

def parse_cookies(cookie_string):
    cookies = []
    for cookie in cookie_string.split('; '):
        if '=' in cookie:
            name, value = cookie.split('=', 1)
            cookie_data = {
                'name': name.strip(),
                'value': value.strip(),
                'path': '/',
                'expires': -1
            }
            
            if name.strip() in ['JSESSIONID', 'userId', 'kuaishou.customer.service.im_st', 'kuaishou.customer.service.im_ph']:
                cookie_data['domain'] = 'adim.kuaishou.com'
            elif name.strip() == 'bUserId':
                cookie_data['domain'] = 'id.kuaishou.com'
            else:
                cookie_data['domain'] = '.kuaishou.com'
            
            cookies.append(cookie_data)
    return cookies

async def create_session(p, cookie_string, account_name, target_url):
    # 浏览器配置
    context = await p.chromium.launch_persistent_context(
        user_data_dir=f"./user_data_{account_name}",
        headless=True,
        args=[
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
            "--disable-dev-shm-usage",
            "--blink-settings=imagesEnabled=false",
            "--disable-extensions",
            "--disable-plugins",
            "--disable-notifications",
        ]
    )
    
    await context.add_cookies(parse_cookies(cookie_string))
    
    page = await context.new_page()
    await page.goto(target_url, timeout=15000) 
    await page.wait_for_load_state("domcontentloaded", timeout=10000) 
    
    if "login" in page.url:
        raise Exception(f"账号 {account_name} Cookie失效，未登录成功")
    
    logger.info(f"账号 {account_name} 会话创建成功，当前页面：{page.url}")
    return page, context

# 配置热加载
def check_and_reload_config(config_path="config.json"):
    global config, GLOBAL_CONFIG, ACCOUNTS, config_last_modified, account_sessions
    
    try:
        current_modified = os.path.getmtime(config_path)
        if current_modified <= config_last_modified:
            return False
        
        with config_lock:
            new_config = load_config(config_path)
            
            # 对比账号配置，处理变更
            old_accounts = {acc["name"]: acc for acc in ACCOUNTS}
            new_accounts = {acc["name"]: acc for acc in new_config["accounts"]}
            
            # 处理删除的账号
            for name in set(old_accounts.keys()) - set(new_accounts.keys()):
                asyncio.create_task(close_account_session(name))
            
            # 处理新增或修改的账号
            for name, new_acc in new_accounts.items():
                old_acc = old_accounts.get(name)
                if not old_acc or old_acc["cookie_string"] != new_acc["cookie_string"]:
                    if old_acc:
                        asyncio.create_task(close_account_session(name))
                    asyncio.create_task(create_and_start_account_session(new_acc))
            
            # 更新全局配置
            config = new_config
            GLOBAL_CONFIG = new_config["global"]
            ACCOUNTS = new_config["accounts"]
            config_last_modified = current_modified
            logger.info("配置文件已热加载，账号会话已更新")
            return True
            
    except Exception as e:
        logger.error(f"热加载配置失败: {str(e)}")
        return False

# 会话管理
async def close_account_session(account_name):
    if account_name in account_sessions:
        page, context = account_sessions[account_name]["session"]
        
        # 标记会话为关闭状态
        if "monitoring" in account_sessions[account_name]:
            account_sessions[account_name]["monitoring"] = False
        
        # 关闭上下文
        try:
            await context.close()
        except Exception as e:
            logger.warning(f"账号 {account_name} 上下文关闭失败: {str(e)}")
        
        # 从会话列表中移除
        del account_sessions[account_name]
        logger.info(f"账号 {account_name} 会话已关闭")

async def create_and_start_account_session(account_config, p=None):
    global account_sessions, stop_event
    
    try:
        if p is None:
            p = getattr(main, "playwright_instance", None)
            if p is None:
                logger.error("无法获取playwright实例，账号会话创建失败")
                return
        
        logger.info(f"开始创建账号 {account_config['name']} 的会话...")
        page, context = await create_session(
            p, 
            account_config["cookie_string"], 
            account_config["name"], 
            GLOBAL_CONFIG["target_url"]
        )
        
        # 创建并启动监控任务
        logger.info(f"准备启动账号 {account_config['name']} 的监控任务...")
        task = asyncio.create_task(monitor_page_changes(
            page, 
            account_config["name"], 
            stop_event
        ))
        
        # 存储会话信息，包括监控任务
        account_sessions[account_config["name"]] = {
            "session": (page, context),
            "task": task,
            "config": account_config,
            "monitoring": True
        }
        
        logger.info(f"账号 {account_config['name']} 新会话已创建并启动监控")
        
    except Exception as e:
        logger.error(f"创建账号 {account_config['name']} 会话失败: {str(e)}")

# 配置监听任务
async def config_monitor_task(stop_event, check_interval=10):
    global logger, config_last_modified
    last_log_date = datetime.now().strftime("%Y%m%d")  # 记录当前日志日期
    
    while not stop_event.is_set():
        # 检查配置文件是否有更新
        check_and_reload_config()
        
        # 检查日期是否变化，如果变化则重新初始化日志
        current_date = datetime.now().strftime("%Y%m%d")
        if current_date != last_log_date:
            # 日期已变更，重新初始化日志系统
            logger.info(f"日期变更，切换日志文件至: monitor_{current_date}.log")
            
            # 创建新的 logger 实例
            new_logger = setup_logger()
            
            # 更新全局 logger
            logging.root.handlers = new_logger.handlers
            logger = new_logger
            
            # 记录日志切换成功
            logger.info(f"日志文件已成功切换至: monitor_{current_date}.log")
            last_log_date = current_date
        
        await asyncio.sleep(check_interval)

# 主函数
async def main():
    global stop_event
    
    target_url = GLOBAL_CONFIG["target_url"]
    stop_event = asyncio.Event()
    
    async with async_playwright() as p:
        main.playwright_instance = p
        
        tasks = []
        
        try:
            # 启动配置监控任务
            config_task = asyncio.create_task(config_monitor_task(stop_event))
            tasks.append(config_task)
            
            # 初始化账号会话
            logger.info("开始初始化账号会话...")
            for account in ACCOUNTS:
                logger.info(f"准备创建账号 {account['name']} 的会话...")
                await create_and_start_account_session(account, p)
            
            logger.info("所有会话已启动，开始监控页面变化...")
            logger.info(f"每日回复限制: 每个用户 {GLOBAL_CONFIG['max_daily_replies']} 条消息")
            logger.info(f"动态消息规则: {GLOBAL_CONFIG['welcome_messages']}")
            logger.info("按 Ctrl+C 停止监控")
            
            # 保持主循环运行
            await asyncio.Event().wait()
            
        except KeyboardInterrupt:
            logger.info("程序已接收到停止信号，正在关闭...")
            stop_event.set()
            
            # 有序关闭所有会话
            for name in list(account_sessions.keys()):
                await close_account_session(name)
                
            # 等待所有任务完成
            await asyncio.gather(*tasks, return_exceptions=True)
            
        finally:
            # 再次确认所有会话已关闭
            for name in list(account_sessions.keys()):
                await close_account_session(name)
                
            logger.info("所有会话已关闭")

if __name__ == "__main__":
    asyncio.run(main())